/*
*Copyright 2021-2023 NERCIS
*
*Licensed under the Apache License, Version 2.0 (the "License");
*you may not use this file except in compliance with the License.
*You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*Unless required by applicable law or agreed to in writing, software
*distributed under the License is distributed on an "AS IS" BASIS,
*WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*See the License for the specific language governing permissions and
*limitations under the License.
*/

package cn.ac.nercis.pes.community.support;

import cn.ac.nercis.pes.community.support.theme.ThemeManager;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

import static java.util.ResourceBundle.getBundle;

/**
 * Fxml视图基础类
 * @author zpy
 * @version 1.0.0
 */
public class AbstractFxmlView implements ApplicationContextAware {
    private final FXMLView annotation;
    private final URL resource;
    private final ObjectProperty<Object> presenter =  new SimpleObjectProperty<>();
    private final Optional<ResourceBundle> bundle;

    private ApplicationContext applicationContext;
    private FXMLLoader fxmlLoader;
    private String fxmlRoot;

    /**
     * Instantiates a new abstract fxml view.
     */
    public AbstractFxmlView() {
        // Set the root path to package path
        fxmlRoot = PropertyReaderHelper.determineFilePathFromPackageName(getClass());
        annotation = this.getClass().getAnnotation(FXMLView.class);
        resource = getUrlResource(annotation);
        bundle = getResourceBundle(getBundleName());
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (this.applicationContext != null) {
            return;
        }
        this.applicationContext = applicationContext;
    }

    @Override
    public String toString() {
        return "AbstractFxmlView [presenterProperty=" + presenter + ", bundle=" + bundle + ", resource="
                + resource + ", fxmlRoot=" + fxmlRoot + "]";
    }

    @SneakyThrows
    public Parent load(){
        fxmlLoader = new FXMLLoader(this.resource,this.bundle.orElse(null));
        Parent parent = fxmlLoader.load();
        this.addCssIfAvailable(parent);
        return parent;
    }

    public Object getController(){
        return fxmlLoader.getController();
    }

    public Parent getView() {
        ensureFxmlLoaderInitialized();
        final Parent parent = fxmlLoader.getRoot();
        addCssIfAvailable(parent);
        return parent;
    }

    public void getView(final Consumer<Parent> consumer) {
        CompletableFuture.supplyAsync(this::getView, Platform::runLater).thenAccept(consumer);
    }

    private void ensureFxmlLoaderInitialized() {
        if (fxmlLoader != null) {
            return;
        }
        fxmlLoader = loadSynchronously(resource, bundle);
        presenter.set(fxmlLoader.getController());
    }

    private FXMLLoader loadSynchronously(final URL resource, final Optional<ResourceBundle> bundle) throws IllegalStateException {
        final FXMLLoader loader = new FXMLLoader(resource, bundle.orElse(null));
        loader.setControllerFactory(this::createControllerForType);
        try {
            loader.load();
        } catch (final IOException | IllegalStateException e) {
            throw new IllegalStateException("Cannot load " + getConventionalName(), e);
        }
        return loader;
    }

    private Object createControllerForType(final Class<?> type) {
        return applicationContext.getBean(type);
    }

    private String getStyleSheetName() {
        return fxmlRoot + getConventionalName(".css");
    }

    private Optional<ResourceBundle> getResourceBundle(final String name) {
        try {
            return Optional.of(getBundle(name));
        } catch (final MissingResourceException ex) {
            return Optional.empty();
        }
    }

    void addCssIfAvailable(final Parent parent) {
        final List<String> list = PropertyReaderHelper.get(applicationContext.getEnvironment(), "javafx.css");
        if (!list.isEmpty()) {
            list.forEach(css -> parent.getStylesheets().add(Objects.requireNonNull(getClass().getResource(css)).toExternalForm()));
        }
        parent.getStylesheets().add(Objects.requireNonNull(getClass().getResource(ThemeManager.CURRENT.getCss())).toExternalForm());
        addCssFromAnnotation(parent);
        final URL uri = getClass().getResource(getStyleSheetName());
        if (Objects.nonNull(uri)) {
            parent.getStylesheets().add(uri.toExternalForm());
        }
    }

    private void addCssFromAnnotation(final Parent parent) {
        if (Objects.nonNull(annotation)) {
            for (final String cssFile : annotation.css()) {
                final URL uri = getClass().getResource(cssFile);
                if (uri != null) {
                    final String uriToCss = uri.toExternalForm();
                    parent.getStylesheets().add(uriToCss);
                }
            }
        }
    }

    private String getBundleName() {
        if (StringUtils.isEmpty(annotation.bundle())) {
            return getClass().getPackage().getName() + "." + getConventionalName();
        }
        return annotation.bundle();
    }

    private URL getUrlResource(final FXMLView annotation) {
        if (Objects.nonNull(annotation) && StringUtils.isNotBlank(annotation.value())) {
            return getClass().getResource(annotation.value());
        } else {
            return getClass().getResource(getFxmlPath());
        }
    }

    private String getFxmlPath() {
        return fxmlRoot + getConventionalName(".fxml");
    }
    private String getConventionalName(final String ending) {
        return getConventionalName() + ending;
    }

    private String getConventionalName() {
        return stripEnding(getClass().getSimpleName().toLowerCase());
    }

    private static String stripEnding(final String clazz) {
        if (!StringUtils.endsWith(clazz,"view")) {
            return clazz;
        }

        return clazz.substring(0, clazz.lastIndexOf("view"));
    }

}